# -*- coding: utf-8 -*-
#
# fusion.tracker.PyBencode
#
# Copyright 2006-2007 Helsinki Institute for Information Technology
# and the authors.
#
# Authors: Harri Hämäläinen <harri.hamalainen@helsinki.fi>
#

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import re

__all__ = ['PyBencode']

def unitTest():
    '''
    >>> bencoder = PyBencode()
    >>> mydict = {'storage': {'ordered': ['pneumatic hammer', 'nail polisher'], 'foods': {'cheese': 4, 'fat milk': 5}, 'tools': {'shovel': 2}}, 'apotek': {'workers': ['henry', 'sally'], 'pills': 10}}
    >>> bencoded_str = bencoder.encode(mydict)
    >>> bencoded_str
    'd6:apotekd5:pillsi10e7:workersl5:henry5:sallyee7:storaged5:foodsd6:cheesei4e8:fat milki5ee7:orderedl16:pneumatic hammer13:nail polishere5:toolsd6:shoveli2eeee'
    >>> mydict2 = bencoder.decode(bencoded_str)
    >>> bencoded_str2 = bencoder.encode(mydict2)
    >>> bencoded_str2
    'd6:apotekd5:pillsi10e7:workersl5:henry5:sallyee7:storaged5:foodsd6:cheesei4e8:fat milki5ee7:orderedl16:pneumatic hammer13:nail polishere5:toolsd6:shoveli2eeee'
    >>> bencoded_str == bencoded_str2
    True
    >>> mydict == mydict2
    True
    '''
    import doctest
    doctest.testmod()

class PyBencode(object):
    def decode(self, input):
        '''
        >>> obj = PyBencode()
        >>> obj.decode('')
        Traceback (most recent call last):
        ...
        SyntaxError: Bad syntax
        >>> obj.decode('d7:storaged5:foodsd6:cheesei4e8:fat milki5ee7:orderedl16:pneumatic hammer13:nail polishere5:toolsd6:shoveli2eeee')
        {'storage': {'tools': {'shovel': 2}, 'foods': {'cheese': 4, 'fat milk': 5}, 'ordered': ['pneumatic hammer', 'nail polisher']}}
        >>> obj.decode('d5:filesd20:12345678901234567890d8:completei0e10:downloadedi0e10:incompletei42eeee')
        {'files': {'12345678901234567890': {'downloaded': 0, 'complete': 0, 'incomplete': 42}}}
        '''
        
        try:
            tokenizer = self._decode_bencode(input)
            result = self._make_dict(tokenizer.next, tokenizer.next())
            for token in tokenizer:
                raise SyntaxError("Trailing junk")
            
        except (AttributeError, StopIteration, ValueError):
            raise SyntaxError("Bad syntax")

        return result
    def _decode_bencode(self, input):

        pattern = re.compile("([dlie])|(\d+):|(-?\d+)").match

        i = 0
        
        while i < len(input):
            m = pattern(input, i)
            s = m.group(m.lastindex)
            i = m.end()

            if m.lastindex == 2:
                yield 's'
                yield input[i:i+int(s)]
                i = i + int(s)
            else:
                yield s

    def _make_dict(self, next, current):
        if current == 'i':
            data = int(next())
            if next() != 'e': raise ValueError
            
        elif current == 's':
            data = next()
            
        elif current == 'd' or current == 'l':
            data = []
            tok = next()
            while tok != 'e':
                data.append(self._make_dict(next, tok))
                tok = next()
            if current == 'd':
                data = dict(zip(data[0::2], data[1::2]))
                
        else: raise ValueError

        return data

    
    def encode(self, input):
        '''
        >>> obj = PyBencode()
        >>> obj.encode('')
        Traceback (most recent call last):
        ...
        TypeError
        >>> obj.encode({'storage': {'ordered': ['pneumatic hammer', 'nail polisher'], 'foods': {'cheese': 4, 'fat milk': 5}, 'tools': {'shovel': 2}}})
        'd7:storaged5:foodsd6:cheesei4e8:fat milki5ee7:orderedl16:pneumatic hammer13:nail polishere5:toolsd6:shoveli2eeee'
        >>> obj.encode({'files': {'12345678901234567890': {'downloaded': 0, 'complete': 0, 'incomplete': 42}}})
        'd5:filesd20:12345678901234567890d8:completei0e10:downloadedi0e10:incompletei42eeee'
        '''
        if len(input) == 0: raise TypeError
        return self._encode_bencode(input)
    
    def _encode_bencode(self, s):
        if isinstance(s, dict):
            ret = 'd'
            dkeys = s.keys()
            dkeys.sort()
            for index in dkeys:
                ret += str(len(index))+':'+index
                ret += str(self._encode_bencode(s[index]))
            ret += 'e'
            return ret
        elif isinstance(s, int) or isinstance(s, long):
            return 'i'+str(s)+'e'
        elif isinstance(s, str) or isinstance(s, unicode):
            return str(len(s))+':'+s
        elif isinstance(s, list):
            ret = 'l'
            for index in s:
                ret += str(self._encode_bencode(index))
            ret += 'e'
            return ret
        else: raise TypeError


if __name__ == '__main__':

    import sys
    operations = ['--run-tests', '--encode', '--decode']

    if sys.argv[1] not in operations:
        print "No such operations."

    if sys.argv[1] == '--run-tests':
        unitTest()

    elif len(sys.argv) < 2:
        print "No input"

    else:
        try:
            file = open(sys.argv[2], 'r')
            input = file.read()
        except IOError:
            input = sys.argv[2]

        bencoder = PyBencode()
        if sys.argv[1] == '--decode':
            retval = bencoder.decode(input)
            for key in retval:
                print key+': '+str(retval[key])
